﻿#if UNITY_PS5
#define CONVERT_PATH
#endif
using Devil;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

namespace GameToolkit.Editor
{
    public partial class MakeABUtil
    {
        struct MovAssetAB
        {
            public string path;
            public int fromAb;
            public int toAb;
        }

        class SearchAssetsForBuild : IYieldCommand
        {
            internal bool searchDependency;
            internal readonly object sync_lock;
            internal readonly Dictionary<int, ABData> abMap;
            internal readonly HashSet<string> mainAssets;
            internal MakeABNode makeNode;

            internal bool canceled;
            internal readonly MakeABConfig config;
            // 定义最终移动资源 ab
            internal readonly List<ABPostprocess> postprocess;
            internal readonly List<MovAssetAB> moves;

            internal SearchAssetsForBuild(bool searchDependency, MakeABConfig config)
            {
                sync_lock = new object();
                abMap = new Dictionary<int, ABData>();
                mainAssets = new HashSet<string>();
                this.searchDependency = searchDependency;
                this.config = config;
                postprocess = new List<ABPostprocess>();
                moves = new List<MovAssetAB>();
            }

            void Cancel()
            {
                canceled = true;
                Debug.Log("Searching assets for build was canceled.");
            }

            void LoadABPostProcess()
            {
                Stream file = null;
                try
                {
                    var filepath = "Assets/AssetBundle.postprocess";
                    if (!NativeFileSystem.HasFile(filepath))
                        return;
                    file = NativeFileSystem.OpenFile(filepath, FileMode.Open, FileAccess.Read);
                    var reader = new StreamReader(file, Encoding.UTF8);
                    string str;
                    ABPostprocess proc = null;
                    while ((str = reader.ReadLine()) != null)
                    {
                        str = str.Trim();
                        if (string.IsNullOrEmpty(str))
                        {
                            proc = null;
                            continue;
                        }
                        var c = str[0];
                        if (c == '@')
                        {
                            str = str.Substring(1);
                            proc = new ABPostprocess(str);
                            postprocess.Add(proc);
                        }
                        else if (proc != null)
                        {
                            ABProcessPattern pattern;
                            if (c == '-')
                            {
                                pattern.isRegex = str.Length > 2 && str[1] == '-';
                                pattern.pattern = pattern.isRegex ? str.Substring(2) : str.Substring(1);
                                proc.AddExcludePattern(pattern);
                            }
                            else if (c == '+')
                            {
                                pattern.isRegex = str.Length > 2 && str[1] == '+';
                                pattern.pattern = pattern.isRegex ? str.Substring(2) : str.Substring(1);
                                proc.AddIncludePattern(pattern);
                            }
                            else
                            {
                                pattern.isRegex = false;
                                pattern.pattern = str;
                                proc.AddIncludePattern(pattern);
                            }
                        }
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                finally
                {
                    if (file != null)
                        file.Close();
                }
            }

            // 执行最后的资源移动
            void OnABPostProcess(ABData ab)
            {
                var lst = new List<MovAssetAB>();
                foreach (var path in ab.assets)
                {
                    for (int i = 0; i < postprocess.Count; i++)
                    {
                        var proc = postprocess[i];
                        if (!abMap.ContainsKey(proc.abid))
                            continue;
                        var match = proc.MatchAsset(path);
                        if (match == ProcessMatchResult.Include && proc.abid != ab.id)
                        {
                            MovAssetAB mov;
                            mov.path = path;
                            mov.fromAb = ab.id;
                            mov.toAb = proc.abid;
                            lst.Add(mov);
                            break;
                        }
                        else if (match == ProcessMatchResult.Exclude && proc.abid == ab.id)
                        {
                            MovAssetAB mov;
                            mov.path = path;
                            mov.fromAb = ab.id;
                            mov.toAb = 0;
                            lst.Add(mov);
                            break;
                        }
                    }
                }
                if (lst.Count > 0)
                {
                    lock (moves)
                    {
                        moves.AddRange(lst);
                    }
                }
            }

            void ExecuteMovAssets()
            {
                var abs = new HashSet<ABData>();
                foreach (var mov in moves)
                {
                    ABData from, to;
                    if (abMap.TryGetValue(mov.fromAb, out from))
                    {
                        if (mov.toAb == 0)
                            to = null;
                        else
                            abMap.TryGetValue(mov.toAb, out to);
                        from.assets.Remove(mov.path);
                        abs.Add(from);
                        if (to != null)
                        {
                            abs.Add(to);
                            to.assets.Add(mov.path);
                        }
                    }
                }
                foreach (var ab in abs)
                {
                    if (ab.assets.Count == 0)
                    {
                        abMap.Remove(ab.id);
                    }
                }
            }

            public IEnumerator Execute()
            {
                var t = System.DateTime.UtcNow.Ticks;
                bool cancel = false;
                var steps = searchDependency ? 3 : 2;
                var step = 1;
                var title = string.Format("Search Assets For Build (step {0}/{1})", step++, steps);
                cancel = EditorUtility.DisplayCancelableProgressBar(title, "Process Assets Folders...", 0.05f);

                if (cancel)
                {
                    Cancel();
                    yield break;
                }
                yield return ParallelUtils.Schedule(AssetsUtil.InitializeUtilInstance, true);
                //AssetsUtil.VariantsForEditor.Initialize();
                makeNode = MakeAB.ParseAssetsTree(false);
                if (makeNode == null)
                {
                    Cancel();
                    yield break;
                }
                var searchFolders = new List<string>();
                if (makeNode.children != null)
                {
                    foreach (var node in makeNode.children)
                    {
                        searchFolders.Add(node.folder);
                    }
                }
                if (searchFolders.Count == 0)
                {
                    Cancel();
                    yield break;
                }
                var loadPostProcessFile = ParallelUtils.Schedule(LoadABPostProcess, false);
                yield return null;
                var assets = AssetDatabase.FindAssets(GetAssetFilter(config), searchFolders.ToArray());
                cancel = EditorUtility.DisplayCancelableProgressBar(title, "Process Assets Folders...", 0.1f);
                if (cancel)
                {
                    Cancel();
                    yield break;
                }
                for (int i = 0; i < assets.Length; i++)
                {
                    assets[i] = AssetDatabase.GUIDToAssetPath(assets[i]);
                }
                //Dictionary<int, ABData> abmap = new Dictionary<int, ABData>();
                //HashSet<string> allAssets = new HashSet<string>();
                //object sync_lock = new object();
                var cmd = new MainAssetProcessor(this);
                var processJob = ParallelUtils.Schedule(cmd, assets);
                while (processJob.IsAlive)
                {
                    yield return null;
                    cancel = EditorUtility.DisplayCancelableProgressBar(title, "Process Assets Folders...", 0.1f + processJob.GetExecution().progress * 0.9f);
                    if (cancel)
                    {
                        processJob.Abort(true);
                        Cancel();
                        yield break;
                    }
                }
                if (searchDependency)
                {
                    title = string.Format("Search Dependency for Build (step {0}/{1})", step++, steps);
                    cancel = EditorUtility.DisplayCancelableProgressBar(title, "Process Assets' Dependency...", 0f);
                    if (cancel)
                    {
                        Cancel();
                        yield break;
                    }
                    var k_process = mainAssets.Count > 0 ? 1f / (float)mainAssets.Count : 1f;
                    int num = 0;
                    var batchNum = 0;
                    var batch = ParallelUtils.NewCombination("Process Dependency Assets");
                    foreach (var path in mainAssets)
                    {
                        num++;
                        cancel = batchNum == 0 && EditorUtility.DisplayCancelableProgressBar(title, path, num * k_process);
                        if (cancel)
                        {
                            Cancel();
                            yield break;
                        }
                        var dependents = AssetDatabase.GetDependencies(path, true);
                        if (dependents == null || dependents.Length == 0)
                            continue;
                        batchNum += dependents.Length;
                        var job = ParallelUtils.Schedule(new DependencyAssetProcessor(this, path, dependents), dependents.Length);
                        batch.Combine(job);
                        if (batchNum > 2000)
                        {
                            batchNum = 0;
                            yield return null;
                        }
                    }
                    while (batch.IsAlive)
                    {
                        yield return null;
                        cancel = EditorUtility.DisplayCancelableProgressBar(title, "Process Assets' Dependency...", batch.GetHandler().GetExecution().progress);
                        if(cancel)
                        {
                            Cancel();
                            yield break;
                        }
                    }
                    //yield return batch;
                }
                // post process ab
                title = string.Format("Saving AB map (step {0}/{1}", step++, steps);
                cancel = EditorUtility.DisplayCancelableProgressBar(title, "Post-process assets' path...", 0f);
                yield return loadPostProcessFile;
                if (postprocess.Count > 0)
                {
                    processJob = ParallelUtils.ScheduleParallel<ABData>(OnABPostProcess, abMap.Values);
                    while (processJob.IsAlive)
                    {
                        cancel = EditorUtility.DisplayCancelableProgressBar(title, "Post-process assets' path...", 0.09f + processJob.GetExecution().progress * 0.9f);
                        if (cancel)
                        {
                            processJob.Abort(true);
                            Cancel();
                            yield break;
                        }
                        yield return null;
                    }
                    cancel = EditorUtility.DisplayCancelableProgressBar(title, "output file Assets/AssetBundleInfo.text", 0.99f);
                    if (cancel)
                    {
                        Cancel();
                        yield break;
                    }
                    yield return ParallelUtils.Schedule(ExecuteMovAssets, false);
                }
                SaveAbmap(abMap);
                var time = (System.DateTime.UtcNow.Ticks - t) * ParallelDispatcher.tick_2_seconds;
                Debug.LogFormat("Finish searching assets for build in {0:0.######} sec", time);
            }

        }

        class MainAssetProcessor : ICommandParallelFor<string>
        {
            internal SearchAssetsForBuild mainCmd;
            internal MainAssetProcessor(SearchAssetsForBuild cmd)
            {
                this.mainCmd = cmd;
            }

            public void Execute(string path)
            {
                var node = MakeABNode.FindNode(mainCmd.makeNode, path);
                if (node == null || node.asset == null || node.path == path)
                {
                    // Debug.LogErrorFormat("skip assets: {0}", path);
                    return;
                }
                if (!node.asset.IsIncludeAsset(node.folder, path, null))
                    return;
                ABData ab;
                var abname = node.asset.GetABName(mainCmd.makeNode, node.folder, path);
                var abid = ParallelUtils.IgnoreCaseToHash(abname);
                var isScene = path.EndsWithIgnoreCase(".scene");
                lock (mainCmd.sync_lock)
                {
                    if (!mainCmd.mainAssets.Add(path))
                        return;
                    var exist = mainCmd.abMap.TryGetValue(abid, out ab);
                    Debug.AssertFormat(!exist || !isScene, "场景AB(\"{0}\") 必须定义为唯一资源", abname);
                    if (!exist)
                    {
                        AssertAbName(abname);
                        ab = new ABData(abid, abname);
                        mainCmd.abMap[abid] = ab;
                    }
                }
                lock (ab)
                {
                    ab.assets.Add(path);
                }
            }
        }

        class DependencyAssetProcessor : ICommandParallelFor<int>
        {
            internal SearchAssetsForBuild mainCmd;
            internal string path;
            string[] dependents;

            internal DependencyAssetProcessor(SearchAssetsForBuild cmd, string asset, string[] dependents)
            {
                this.mainCmd = cmd;
                this.path = asset;
                this.dependents = dependents;
            }

            public void Execute(int index)
            {
                var dependency = dependents[index];
                if (dependency.EndsWith(".cs") || !File.Exists(dependency))
                    return;
                string abname;
                if (dependency.StartsWithIgnoreCase("packages/com."))
                {
                    var i0 = dependency.IndexOf('/');
                    var i1 = dependency.IndexOf('/', i0 + 1);
                    if (!(i0 > 0 && i1 > i0) || dependency.ContainsIgnoreCase("/editor/"))
                        return;
                    var node = mainCmd.makeNode.upmAsset;
                    if (node != null && node.asset != null && !node.asset.IsIncludeAsset(node.folder, dependency, path))
                        return;
                    abname = node == null || node.asset == null ? "Packages/UPM_assets" :
                        node.asset.GetABName(mainCmd.makeNode, node.folder, dependency);
                }
                else
                {
                    var node = MakeABNode.FindNode(mainCmd.makeNode, dependency);
                    if (node == null || node.asset == null || node.path == dependency)
                        return;
                    if (!node.asset.IsIncludeAsset(node.folder, dependency, path))
                        return;
                    abname = node.asset.GetABName(mainCmd.makeNode, node.folder, dependency);
                }
                ABData ab;
                var abid = ParallelUtils.IgnoreCaseToHash(abname);
                lock (mainCmd.sync_lock)
                {
                    if (!mainCmd.abMap.TryGetValue(abid, out ab))
                    {
                        AssertAbName(abname);
                        ab = new ABData(abid, abname);
                        mainCmd.abMap[abid] = ab;
                    }
                }
                lock (ab)
                {
                    ab.assets.Add(dependency);
                }
            }
        }

        class BuildAB : IYieldCommand, IBreakCommandSignal
        {
            internal bool searchAssets;
            List<AssetBundleBuild> abs;
            readonly object sync_lock = new object();
            MakeABConfig config;
            string rootFolder;
            bool isEditingAsset;

            internal BuildAB(bool search, MakeABConfig config)
            {
                this.searchAssets = search;
                abs = new List<AssetBundleBuild>();
                this.config = config;
            }

            void SetupAB(ABData ab)
            {
                if (ab.assets.Count == 0)
                    return;
                var streaming = ".streaming";
                if (ab.abName.EndsWithIgnoreCase(streaming))
                {
                    var folder = NativeFileSystem.Combine(rootFolder, ab.abName.Substring(0, ab.abName.Length - streaming.Length));
                    if (!Directory.Exists(folder))
                        Directory.CreateDirectory(folder);
                    foreach (var asset in ab.assets)
                    {
                        var path = NativeFileSystem.Combine(folder, NativeFileSystem.GetFileName(asset, false));
                        NativeFileSystem.CopyFile(asset, path);
                    }
                }
                else
                {
                    AssetBundleBuild build;
                    build.assetBundleName = ab.abName;
                    build.assetBundleVariant = "";
                    build.addressableNames = null;
                    var assets = new string[ab.assets.Count];
                    ab.assets.CopyTo(assets);
                    build.assetNames = assets;
                    string abname, variant;
                    var keyword = AssetsUtil.VariantsForEditor.GetABInfo(ab.abName, out abname, out variant);
                    if (keyword != null)
                    {
                        var address = new string[assets.Length];
                        //var n = ab.abName.LastIndexOf('.');
                        //var varname = ab.abName.Substring(n + 1);
                        //var regex = ABVariant.GetVariantAssetReplacePattern(varname);
                        // string.Format(@"/[\w\W\d_ ]+\.{0}/", varname);
                        //MatchEvaluator repalce = ABVariant.VariantAssetEvaluator;
                        for (int i = 0; i < assets.Length; i++)
                        {
                            //var match = regex.Replace(assets[i], repalce);
                            //address[i] = match;
                            address[i] = keyword.GetAssetAddress(assets[i]);
                        }
                        build.addressableNames = address;
                    }
                    lock (sync_lock)
                    {
                        abs.Add(build);
                    }
                }
            }

            void Cancel()
            {
                EditorUtility.ClearProgressBar();
                Debug.Log("Build AssetBundle was canceled.");
            }

            public IEnumerator Execute()
            {
                AssetsUtil.mIsBuildingAsset = true;
                //var variantCmd = ABVariant.InitVariants(false);
                var variantCmd = ParallelUtils.Schedule(AssetsUtil.InitializeUtilInstance, true);
                var t = System.DateTime.UtcNow.Ticks;
                bool cancel = false;
                var platform = config.m_BuildTarget;// (BuildTarget)EditorPrefs.GetInt("MakeAB.platform", (int)GetCurrentPlatform());
                if (platform == BuildTarget.NoTarget)
                    platform = GetCurrentPlatform();
                var dest = config.m_Destination;
                // EditorPrefs.GetString("MakeAB.output", "Assets/StreamingAssets");// Path.Combine(Application.dataPath, "StreamingAssets", targetPlatform.ToString());
                if (!dest.EndsWithIgnoreCase(platform.ToString()))
                    dest = NativeFileSystem.Combine(dest, platform.ToString());
                rootFolder = dest;
                if (!Directory.Exists(dest))
                    Directory.CreateDirectory(dest);
                var option = config.m_BuildOption;// (BuildAssetBundleOptions)EditorPrefs.GetInt("MakeAB.option", 0);
                                                  //#if UNITY_2020_1_OR_NEWER
                                                  //#else
#if CHINA_VERSION
#if ENCRYPT_ENABLED
                option |= BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.EnableProtection;
                option &= ~BuildAssetBundleOptions.UncompressedAssetBundle;
#else
                option &= ~BuildAssetBundleOptions.EnableProtection;
#endif
#endif
                //#endif
                //MakeABNode makenode = null;
                Dictionary<int, ABData> abmap = null;

                yield return variantCmd;
                if (searchAssets || !ReadAbmap(ref abmap))
                {
                    var searchCmd = new SearchAssetsForBuild(true, config);
                    yield return ParallelUtils.Schedule(searchCmd, true);
                    abmap = searchCmd.abMap;
                    if (searchCmd.canceled)
                    {
                        Cancel();
                        yield break;
                    }
                }
                if (abmap != null && abmap.Count > 0)
                {
                    var setup = ParallelUtils.ScheduleParallel<ABData>(SetupAB, abmap.Values);
                    while (setup.IsAlive)
                    {
                        cancel = EditorUtility.DisplayCancelableProgressBar("Setup AB", "Collect AssetBundle infos...", setup.GetExecution().progress);
                        if (cancel)
                        {
                            setup.Abort(true);
                            Cancel();
                            yield break;
                        }
                        yield return null;
                    }
                }
                yield return null;
                cancel = EditorUtility.DisplayCancelableProgressBar("Building AssetBundle", "It's going to build assetbundle...", 0f);
                if (cancel)
                {
                    Cancel();
                    yield break;
                }
                yield return null;
                string key;
#if CHINA_VERSION
                var enopt = BuildAssetBundleOptions.EnableProtection | BuildAssetBundleOptions.ChunkBasedCompression;
                var encript = (option & enopt) == enopt && !string.IsNullOrEmpty(MakeAB.DEFAULT_KEY);
                if (encript)
                {
                    key = MakeAB.DEFAULT_KEY;
                    BuildPipeline.SetAssetBundleEncryptKey(MakeAB.DEFAULT_KEY);
                    //Debug.LogFormat("Will build asset bundle with KEY: {0}", MakeAB.DEFAULT_KEY);
                }
                else
                {
                    key = "";
                    option &= ~BuildAssetBundleOptions.EnableProtection;
                    BuildPipeline.SetAssetBundleEncryptKey(null);
                    //Debug.Log("Will build asset bundle without KEY");
                }
#else
                key = "";
#endif
                BuildPipeline.BuildAssetBundles(dest, abs.ToArray(), option, platform);

                AssetDatabase.StartAssetEditing();
                isEditingAsset = true;

                var jobs = ParallelUtils.NewCombination("Complete AB Making");
                var cmd = new BuildABTree(dest);
                jobs.Combine(ParallelUtils.Schedule(cmd, true));

                // clear invalid files
                var cleaner = new CleanupUnsedAbFiles(abmap, dest);
                jobs.Combine(ParallelUtils.Schedule(cleaner, false));

                yield return jobs;

                AssetDatabase.StopAssetEditing();
                isEditingAsset = false;

                EditorUtility.ClearProgressBar();

                Debug.LogFormat(@"Build AssetBundle Complete. 
    platform: {0}
    option: {1}
    output: {2}
    ab count: {3}
    key: {4}",
                    platform,
                    ParallelUtils.ToMask<BuildAssetBundleOptions>((int)option, ","),
                    dest,
                    abmap == null ? 0 : abmap.Count,
                    key);
                AssetsUtil.mIsBuildingAsset = false;
            }

            void IBreakCommandSignal.OnBreak()
            {
                AssetsUtil.mIsBuildingAsset = false;
                EditorUtility.ClearProgressBar();
                if (isEditingAsset)
                {
                    isEditingAsset = false;
                    AssetDatabase.StopAssetEditing();
                }
            }
        }

        class BuildABTree : IYieldCommand, IBreakCommandSignal
        {
            bool encrypt;
            Stream stream;
            string folder;

            string extension;
            int extensionLen;
            Queue<List<string>> abInfos;


            internal BuildABTree(string dest = null)
            {
                this.folder = dest;
#if CHINA_VERSION && ENCRYPT_ENABLED
                encrypt = true;
#endif
                extension = ".manifest";
                extensionLen = extension.Length;
                abInfos = new Queue<List<string>>();
            }

            void ParseManifest(string path)
            {
                var ab = NativeFileSystem.CalculateRelativePath(folder, path);
                if (string.IsNullOrEmpty(ab))
                    return;
                ab = ab.Substring(0, ab.Length - extensionLen);

#if CONVERT_PATH
                ab = ab.Replace('\\', '/');
#endif
                var lst = new List<string>();
                lst.Add(ab);
                var manifestKey = new Regex("^ *\\w+ *:.*$", RegexOptions.IgnoreCase);
                var manifestPath = new Regex(@"^- [c-z]:(/|\\).+ *$", RegexOptions.IgnoreCase);
                using (var manifest = File.OpenRead(path))
                {
                    using (var reader = new StreamReader(manifest))
                    {
                        string str;
                        bool catchDependency = false;
                        while ((str = reader.ReadLine()) != null)
                        {
                            if (manifestKey.IsMatch(str))
                            {
                                if (catchDependency)
                                    break;
                                else if (str.Contains("Dependencies:"))
                                    catchDependency = true;
                            }
                            else if (catchDependency && manifestPath.IsMatch(str))
                            {
                                str = NativeFileSystem.CalculateRelativePath(folder, str.Substring(2));
#if CONVERT_PATH
                                str = str.Replace('\\', '/');
#endif
                                lst.Add(str);
                            }
                        }
                    }
                }
                lock (abInfos)
                {
                    abInfos.Enqueue(lst);
                }
            }

            public IEnumerator Execute()
            {
                // generate assetbundle tree
                EditorUtility.DisplayProgressBar("Build AB Tree", "Loading Manifest...", 0.1f);

                if (string.IsNullOrEmpty(folder))
                {
                    MakeABConfig config = ProjectConfiguration.GetConfigurable<MakeABConfig>();
                    var platform = config.m_BuildTarget;// (BuildTarget)EditorPrefs.GetInt("MakeAB.platform", (int)GetCurrentPlatform());
                    if (platform == BuildTarget.NoTarget)
                        platform = GetCurrentPlatform();
                    var dest = config.m_Destination;
                    // EditorPrefs.GetString("MakeAB.output", "Assets/StreamingAssets");// Path.Combine(Application.dataPath, "StreamingAssets", targetPlatform.ToString());
                    if (!dest.EndsWithIgnoreCase(platform.ToString()))
                        dest = NativeFileSystem.Combine(dest, platform.ToString());
                    folder = dest;
                }
#if CHINA_VERSION
                if (encrypt && !string.IsNullOrEmpty(MakeAB.DEFAULT_KEY))
                    AssetBundle.SetAssetBundleDecryptKey(MakeAB.DEFAULT_KEY);
                else
                    AssetBundle.SetAssetBundleDecryptKey(null);
#endif
                //var abFile = NativeFileSystem.Combine(folder, GlobalUtil.GetFileName(folder, false));
                //var metaAb = AssetBundle.LoadFromFileAsync(abFile);
                //yield return new UnityInstruction(metaAb);
                //Debug.AssertFormat(metaAb.assetBundle != null, "Can't load ab from file: {0}", abFile);
                //var meta = metaAb.assetBundle.LoadAssetAsync<AssetBundleManifest>("AssetBundleManifest");
                //yield return new UnityInstruction(meta);
                //var manifest = meta.asset as AssetBundleManifest;

                Stack<DirectoryInfo> dics = new Stack<DirectoryInfo>();
                dics.Push(new DirectoryInfo(folder));
                var manifests = new List<string>();
                while (dics.Count > 0)
                {
                    var dic = dics.Pop();
                    var files = dic.GetFiles();
                    foreach (var info in files)
                    {
                        var path = info.FullName;
                        if (path.EndsWithIgnoreCase(extension))
                        {
                            manifests.Add(path);
                        }
                    }
                    var subFolder = dic.GetDirectories();
                    foreach (var info in subFolder)
                    {
                        dics.Push(info);
                    }
                }
                var parseManifest = ParallelUtils.ScheduleParallel<string>(ParseManifest, manifests);
                yield return parseManifest;

                var file = "Assets/AssetBundleInfo.tree";
                stream = File.Open(file, FileMode.Create, FileAccess.Write);
                var writer = new StreamWriter(stream);
                writer.WriteLine(System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ddd\n"));

                while (abInfos.Count > 0)
                {
                    var lst = abInfos.Dequeue();
                    writer.WriteLine(ParallelUtils.Concat('@', lst[0]));
                    for (int i = 1; i < lst.Count; i++)
                    {
                        writer.WriteLine(lst[i]);
                    }
                    writer.WriteLine();
                }

                //var buildAbs = manifest.GetAllAssetBundles();
                //var f = buildAbs.Length > 0 ? (1f / buildAbs.Length) : 0f;
                //for (int i = 0; i < buildAbs.Length; i++)
                //{
                //    EditorUtility.DisplayProgressBar("Build AB Tree", StringUtil.Concat("Parse Ab: ", buildAbs[i], "..."), 0.1f + i * f);
                //    writer.WriteLine(StringUtil.Concat('@', buildAbs[i]));
                //    var dependency = manifest.GetDirectDependencies(buildAbs[i]);
                //    for (int k = 0; k < dependency.Length; k++)
                //    {
                //        writer.WriteLine(dependency[k]);
                //    }
                //    writer.WriteLine();
                //}
                //metaAb.assetBundle.Unload(true);
                stream.Flush();
                writer.Dispose();
                stream.Close();
                stream.Dispose();
                EditorUtility.ClearProgressBar();
                Debug.LogFormat("\"{0}\" was writed.", file);
            }

            void IBreakCommandSignal.OnBreak()
            {
                EditorUtility.ClearProgressBar();
                if (stream != null)
                {
                    stream.Dispose();
                }
            }
        }

        class CleanupUnsedAbFiles : ICommand
        {
            string mFolder;
            int manifest;
            Dictionary<int, ABData> abmap;

            internal CleanupUnsedAbFiles(Dictionary<int, ABData> abmap, string dest)
            {
                this.mFolder = dest.Replace('\\', '/');
                if (mFolder.Length > 0 && mFolder[mFolder.Length - 1] == '/')
                    mFolder = mFolder.Substring(0, mFolder.Length - 1);
                this.abmap = abmap;
                manifest = ParallelUtils.IgnoreCaseToHash(NativeFileSystem.GetFileName(dest, false));
            }

            void ICommand.Execute()
            {
                var stack = new Stack<DirectoryInfo>();
                stack.Push(new DirectoryInfo(mFolder));
                List<string> cleanFolders = new List<string>();
                var ignoreFile = new Regex(@"\.manifest(\.meta)?$", RegexOptions.IgnoreCase);
                while (stack.Count > 0)
                {
                    var dic = stack.Pop();
                    var files = dic.GetFiles();
                    var deleteNum = 0;
                    foreach (var f in files)
                    {
                        var path = f.FullName.Replace('\\', '/');
                        var i = path.IndexOf(mFolder);
                        if (i == -1)
                            break;
                        var abname = path.Substring(i + mFolder.Length + 1);
                        var match = ignoreFile.Match(abname);
                        if (match != null && match.Success)
                            abname = abname.Substring(0, match.Index);
                        else if (abname.EndsWithIgnoreCase(".meta"))
                            continue;
                        var id = ParallelUtils.IgnoreCaseToHash(abname);
                        if (id != manifest && !abmap.ContainsKey(id))
                        {
                            File.Delete(f.FullName);
                            Debug.LogFormat("Delete file: \"{0}/{1}\"", mFolder, abname);
                            deleteNum++;
                        }
                    }
                    if (deleteNum == files.Length)
                    {
                        cleanFolders.Add(dic.FullName);
                    }
                    var subdics = dic.GetDirectories();
                    foreach (var sub in subdics)
                    {
                        stack.Push(sub);
                    }
                }
                for (int i = cleanFolders.Count - 1; i >= 0; i--)
                {
                    var dic = new DirectoryInfo(cleanFolders[i]);
                    if (dic.GetDirectories().Length == 0)
                    {
                        Directory.Delete(dic.FullName);
                    }
                }
            }
        }

        // 检查资源 ID 冲突
        class CheckAssetConflict : ICommand
        {
            void SetAsset(Dictionary<int, List<string>> dic, int id, string file)
            {
                List<string> lst;
                if (!dic.TryGetValue(id, out lst))
                {
                    lst = new List<string>();
                    dic.Add(id, lst);
                }
                lst.Add(file);
            }

            public void Execute()
            {
                var f = "Assets/AssetBundleInfo.txt";
                int conflict = 0;
                try
                {
                    var ins = File.OpenRead(f);
                    var reader = new StreamReader(ins);
                    string str;
                    Dictionary<int, List<string>> dupicate = new Dictionary<int, List<string>>();
                    while ((str = reader.ReadLine()) != null)
                    {
                        if (string.IsNullOrEmpty(str))
                            continue;
                        int id;
                        if (str[0] == '@')
                        {
                            str = str.Substring(1);
                            id = AssetBundleUtil.AssetProcessor.IdentifyAsset<AssetBundle>(str);
                            SetAsset(dupicate, id, ParallelUtils.Concat("<AssetBundle>", str));
                        }
                        else if (str.EndsWithIgnoreCase(".prefab"))
                        {
                            id = AssetBundleUtil.AssetProcessor.IdentifyAsset<GameObject>(str);
                            SetAsset(dupicate, id, ParallelUtils.Concat("<GameObj>", str));
                        }
                        id = ParallelUtils.IgnoreCaseToHash(str);
                        SetAsset(dupicate, id, str);

                    }
                    reader.Dispose();
                    ins.Dispose();

                    foreach (var lst in dupicate.Values)
                    {
                        if (lst.Count > 1)
                        {
                            conflict++;
                            var info = ParallelUtils.Gather(lst, ',');
                            Debug.LogFormat("Conflict ID between ({0})", info);
                        }
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                if (conflict == 0)
                    Debug.Log("No conflicts");
                else
                    Debug.LogFormat("Find {0} conflicts", conflict);
            }
        }

        // 导出资源 guid
        class ExportAssetGUID : IYieldCommand, IBreakCommandSignal
        {
            const byte STAT_READ_FILE = 1;
            const byte STAT_GET_GUID = 2;
            const byte STAT_EXPORT_GUID = 3;

            Stream fileStream;
            StreamReader reader;

            Stream outputStream;
            StreamWriter writer;

            bool isReadingPath;
            List<string> assets;

            float readProgress;
            string readingAsset;
            bool abort;
            ParallelDispatcher.Handler mainProgress;

            byte state;
            string version;

            MakeABConfig cfg;
            public ExportAssetGUID(MakeABConfig cfg)
            {
                this.cfg = cfg;
            }

            IEnumerator MainProgress()
            {
                version = Application.version;
                while (!abort)
                {
                    switch (state)
                    {
                        case STAT_READ_FILE:
                            abort |= EditorUtility.DisplayCancelableProgressBar("Export GUID (1/3)", "Preparing...", readProgress);
                            yield return null;
                            break;
                        case STAT_GET_GUID:
                            var num = 0;
                            var f = assets.Count > 0 ? (1d / assets.Count) : 1d;
                            for (int i = assets.Count - 1; !abort && i >= 0; i--)
                            {
                                num++;
                                var assetPath = assets[i];
                                readProgress = (float)(num * f);
                                abort |= EditorUtility.DisplayCancelableProgressBar("Export GUID (2/3)", "Calculate GUID...", readProgress);
                                var guid = AssetDatabase.AssetPathToGUID(assetPath);
                                if (!string.IsNullOrEmpty(guid))
                                {
                                    //assets[i] = string.Format("{0}: {1}", guid, assetPath);
                                    var group = AssetsUtil.VariantsForEditor.GetVariantGroup(EVariantGroupQuery.AssetPath, assetPath);
                                    assets[i] = string.Format("{0}: {1}", guid, group == null ? assetPath : group.GetAssetAddress(assetPath));// ABVariant.GetAssetPathWithOutVariant(assetPath));
                                }
                                else
                                {
                                    assets.RemoveAt(i);
                                }
                                if ((num & 255) == 255)
                                    yield return null;
                            }
                            state = STAT_EXPORT_GUID;
                            break;
                        case STAT_EXPORT_GUID:
                            abort |= EditorUtility.DisplayCancelableProgressBar("Export GUID (3/3)", "Saving GUID...", readProgress);
                            yield return null;
                            break;
                        default:
                            abort |= EditorUtility.DisplayCancelableProgressBar("Export GUID (1/3)", "Preparing...", 0);
                            yield return null;
                            break;
                    }
                }
            }

            public IEnumerator Execute()
            {
                var variantCmd = ParallelUtils.Schedule(AssetsUtil.InitializeUtilInstance, true);
                //var variantCmd = ParallelUtils.Schedule(() => AssetsUtil.VariantsForEditor.Initialize(), false);// ABVariant.InitVariants(false);
                readingAsset = "";
                readProgress = 0f;
                mainProgress = ParallelUtils.Schedule(MainProgress, true);

                state = STAT_READ_FILE;
                assets = new List<string>();
                var filePath = "Assets/AssetBundleInfo.txt";
                fileStream = NativeFileSystem.OpenFile(filePath, FileMode.Open, FileAccess.Read);
                reader = new StreamReader(fileStream, Encoding.UTF8);
                string str;
                var len = fileStream.Length;
                double length = len > 0 ? (1.0 / len) : 1.0;
                while (!abort && (str = reader.ReadLine()) != null)
                {
                    readingAsset = str;
                    readProgress = (float)(fileStream.Position * length);
                    if (string.IsNullOrEmpty(str))
                        isReadingPath = false;
                    else if (isReadingPath)
                        assets.Add(str);
                    else if (str[0] == '@')
                        isReadingPath = true;
                }

                yield return variantCmd;
                state = STAT_GET_GUID;

                while (state == STAT_GET_GUID)
                {
                    yield return null;
                }

                var folder = string.IsNullOrEmpty(cfg.m_GUIDFolder) ? "Assets/../GUID" : cfg.m_GUIDFolder;
                NativeFileSystem.EnsureDirectory(folder);
                filePath = NativeFileSystem.Combine(folder, string.Format("{0}.txt", version));
                outputStream = NativeFileSystem.OpenFile(filePath, FileMode.Create, FileAccess.Write);
                writer = new StreamWriter(outputStream, Encoding.UTF8);
                length = assets.Count > 0 ? (1.0 / assets.Count) : 1.0;
                readProgress = 0;
                for (int i = 0; i < assets.Count; i++)
                {
                    readProgress = (float)(i * length);
                    writer.WriteLine(assets[i]);
                }
                writer.Flush();
                OnBreak();
                MainThread.RunOnMainThread(PopupReportDialog, string.Format("已导出资源GUID文件\nfile: {0}", filePath));
            }

            void PopupReportDialog(string content)
            {
                EditorUtility.DisplayDialog("Export GUID", content, "确定");
            }

            public void OnBreak()
            {
                mainProgress.Abort();
                if (fileStream != null)
                    fileStream.Dispose();
                if (outputStream != null)
                    outputStream.Dispose();
                fileStream = null;
                reader = null;
                outputStream = null;
                writer = null;
                MainThread.RunOnMainThread(() => EditorUtility.ClearProgressBar());
            }
        }

        class AssetCmdBase
        {
            protected Dictionary<string, string> guidToPath;
            protected void ReadAssetBundleInfo()
            {
                Stream stream = null;
                try
                {
                    guidToPath = new Dictionary<string, string>();
                    var file = "Assets/AssetBundleInfo.txt";
                    stream = NativeFileSystem.OpenFile(file, FileMode.Open, FileAccess.Read);
                    var reader = new StreamReader(stream, Encoding.UTF8);
                    string str;
                    bool isAsset = false;
                    while ((str = reader.ReadLine()) != null)
                    {
                        if (string.IsNullOrEmpty(str))
                        {
                            isAsset = false;
                        }
                        else if (isAsset)
                        {
                            var guid = AssetDatabase.AssetPathToGUID(str);
                            if (!string.IsNullOrEmpty(guid))
                            {
                                var group = AssetsUtil.VariantsForEditor.GetVariantGroup(EVariantGroupQuery.AssetPath, str);
                                guidToPath[guid] = group == null ? str : group.GetAssetAddress(str);// ABVariant.GetAssetPathWithOutVariant(str);
                            }
                        }
                        else if (str[0] == '@')
                        {
                            isAsset = true;
                        }
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                finally
                {
                    if (stream != null)
                        stream.Dispose();
                }
            }
        }

        class CleanGUIDCmd : AssetCmdBase, IYieldCommand
        {
            MakeABConfig cfg;
            public CleanGUIDCmd(MakeABConfig cfg)
            {
                this.cfg = cfg;
            }

            IEnumerator IYieldCommand.Execute()
            {
                throw new System.NotImplementedException();
            }
        }

        class RedirectAssetCmd : AssetCmdBase, IYieldCommand, IBreakCommandSignal
        {
            MakeABConfig cfg;

            ParallelDispatcher.Handler cmds;

            Stream outputStream;


            List<string> guidFiles;
            List<Dictionary<string, string>> guidFilesToPath;

            string mVersion;

            public RedirectAssetCmd(MakeABConfig cfg, string ver)
            {
                this.cfg = cfg;
                mVersion = ver;
            }

            void ReadGUIDFile(int index)
            {
                Stream stream = null;
                try
                {
                    var file = guidFiles[index];
                    var dic = guidFilesToPath[index];
                    stream = NativeFileSystem.OpenFile(file, FileMode.Open, FileAccess.Read);
                    var reader = new StreamReader(stream, Encoding.UTF8);
                    string str;
                    while ((str = reader.ReadLine()) != null)
                    {
                        var i = string.IsNullOrEmpty(str) ? -1 : str.IndexOf(':');
                        if (i < 1)
                            continue;
                        dic[str.Substring(0, i)] = str.Substring(i + 1).Trim();
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                finally
                {
                    if (stream != null)
                        stream.Dispose();
                }
            }

            public IEnumerator Execute()
            {
                var folder = string.IsNullOrEmpty(cfg.m_GUIDFolder) ? "Assets/../GUID" : cfg.m_GUIDFolder;
                if (!NativeFileSystem.HasFolder(folder))
                    yield break;
                var dic = new DirectoryInfo(folder);
                var files = dic.GetFiles();
                if (files.Length == 0)
                    yield break;
                var variantCmd = ParallelUtils.Schedule(AssetsUtil.InitializeUtilInstance, true);
                //var variantCmd = ParallelUtils.Schedule(() => AssetsUtil.VariantsForEditor.Initialize(), false);// ABVariant.InitVariants(false);
                guidFiles = new List<string>(files.Length);
                guidFilesToPath = new List<Dictionary<string, string>>(files.Length);
                foreach (var fileInfo in files)
                {
                    var fname = NativeFileSystem.GetFileName(fileInfo.FullName, true);
                    if (ParallelUtils.CompareVersionDefine(fname, cfg.m_RedirectVersion) >= 0)
                    {
                        guidFiles.Add(fileInfo.FullName);
                        guidFilesToPath.Add(new Dictionary<string, string>());
                    }
                }
                var cmd1 = ParallelUtils.Schedule(ReadAssetBundleInfo, true, variantCmd);
                var cmd2 = ParallelUtils.ScheduleParallel(ReadGUIDFile, guidFiles.Count);
                cmds = ParallelUtils.Combine(cmd1, cmd2);
                yield return cmds;
                var redirectFolder = "Assets/AssetsRedirect";
                var redirectFile = $"{redirectFolder}/{mVersion}.redirect";
                var customFile = $"{redirectFolder}/custom.redirect";
                NativeFileSystem.EnsureDirectory(redirectFolder);
                outputStream = NativeFileSystem.OpenFile(redirectFile, FileMode.Create, FileAccess.Write);
                var writer = new StreamWriter(outputStream, Encoding.UTF8);
                string assetPath;
                HashSet<int> duplicats = new HashSet<int>();
                try
                {
                    if (NativeFileSystem.HasFile(customFile))
                    {
                        var pattern = new Regex(@"^[a-z 0-9_/]+ > [a-z 0-9_/]+$", RegexOptions.IgnoreCase);
                        var inFile = NativeFileSystem.OpenFile(customFile, FileMode.Open, FileAccess.Read);
                        var reader = new StreamReader(inFile);
                        string str;
                        while ((str = reader.ReadLine()) != null)
                        {
                            if (pattern.IsMatch(str) && duplicats.Add(str.GetHashCode()))
                                writer.WriteLine(str);
                        }
                        inFile.Dispose();
                        reader.Dispose();
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                for (int i = 0; i < guidFilesToPath.Count; i++)
                {
                    var guidDic = guidFilesToPath[i];
                    foreach (var kv in guidDic)
                    {
                        var guid = kv.Key;
                        if (guidToPath.TryGetValue(guid, out assetPath) && !kv.Value.EqualsIgnoreCase(assetPath))
                        {
                            var str = string.Format("{0} > {1}", kv.Value, assetPath);
                            if (duplicats.Add(str.GetHashCode()))
                                writer.WriteLine(str);
                        }
                    }
                }
                writer.Flush();
                OnBreak();
                MainThread.RunOnMainThread(PopupReportDialog, $"资源路径重定向文件已生成\nfile: {redirectFile}");
            }

            void PopupReportDialog(string content)
            {
                EditorUtility.DisplayDialog("Redirect Assets' Path", content, "确定");
            }

            public void OnBreak()
            {
                cmds.Abort(true);
                if (outputStream != null)
                    outputStream.Dispose();
                outputStream = null;
            }
        }

        class FindABDependencyCircleCmd : IYieldCommand, IBreakCommandSignal, IExecutableProgress
        {
            class AB
            {
                internal readonly int id;
                internal readonly string name;
                internal HashSet<string> dependency;
                public AB(int id, string name)
                {
                    this.id = id;
                    this.name = name;
                    dependency = new HashSet<string>();
                }
            }

            enum DependentNode : byte
            {
                Start,
                Link,
                End,
            }

            struct DependencyLink
            {
                public DependentNode node;
                public AB baseAb;
                public AB dependentOn;
            }

            string outputFile;
            Stream mInputFile;
            Stream mOutputFile;
            Dictionary<int, AB> mAbList;
            HashSet<string> mCircles;

            public FindABDependencyCircleCmd(string outputFile)
            {
                this.outputFile = outputFile;
            }

            public float Progress { get; private set; }

            public void OnBreak()
            {
                try
                {
                    if (mInputFile != null)
                    {
                        mInputFile.Close();
                        mInputFile.Dispose();
                        mInputFile = null;
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
                try
                {
                    if (mOutputFile != null)
                    {
                        mOutputFile.Close();
                        mOutputFile.Dispose();
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                }
            }

            void FindDependencyCircle(AB ab)
            {
                List<DependencyLink> links = new List<DependencyLink>();
                var stack = new Stack<AB>();
                stack.Push(ab);
                var cache = new HashSet<AB>();
                cache.Add(ab);
                while (stack.Count > 0)
                {
                    var it = stack.Pop();
                    foreach (var dep in it.dependency)
                    {
                        AB dependentOn;
                        var id = ParallelUtils.IgnoreCaseToHash(dep);
                        if (!mAbList.TryGetValue(id, out dependentOn))
                            continue;
                        DependencyLink link;
                        link.baseAb = it;
                        link.dependentOn = dependentOn;
                        if (cache.Add(dependentOn))
                            stack.Push(dependentOn);
                        if (dependentOn == ab)
                            link.node = DependentNode.End;
                        else if (it == ab)
                            link.node = DependentNode.Start;
                        else
                            link.node = DependentNode.Link;
                        links.Add(link);
                    }
                }
                var buf = new StringBuilder();
                var circles = new List<string>();
                var linkBuffer = new List<DependencyLink>();
                for (int i = links.Count - 1; i >= 0; i--)
                {
                    var link = links[i];
                    if(link.node == DependentNode.End)
                    {
                        linkBuffer.Clear();
                        linkBuffer.Add(link);
                        for (int j = i - 1; j >= 0; j--)
                        {
                            var prev = links[j];
                            if(prev.dependentOn == link.baseAb && prev.node != DependentNode.End)
                            {
                                linkBuffer.Add(prev);
                                if(prev.node == DependentNode.Start)
                                {
                                    break;
                                }
                                link = prev;
                            }
                        }
                        int startIndex = linkBuffer.Count - 1;
                        var id = linkBuffer[startIndex].baseAb.id;
                        for (int j = 0; j < linkBuffer.Count; j++)
                        {
                            var lid = linkBuffer[j].baseAb.id;
                            if (lid < id)
                            {
                                id = lid;
                                startIndex = i;
                            }
                        }
                        buf.Clear();
                        // a-b, b-c, c-d
                        for (int j = linkBuffer.Count - 1; j >= 0; j--)
                        {
                            var lnode = linkBuffer[(j + startIndex) % linkBuffer.Count];
                            if (buf.Length == 0)
                                buf.Append('\"').Append(lnode.baseAb.name).Append('\"');
                            buf.Append(" -> \"").Append(lnode.dependentOn.name).Append('\"');
                        }
                        circles.Add(buf.ToString());
                    }
                }
                lock (mCircles)
                {
                    for (int i = 0; i < circles.Count; i++)
                        mCircles.Add(circles[i]);
                }
            }

            IEnumerator IYieldCommand.Execute()
            {
                Progress = 0;
                mCircles = new HashSet<string>();
                var abFile = "Assets/AssetBundleInfo.tree";
                if (!NativeFileSystem.HasFile(abFile) || string.IsNullOrEmpty(outputFile))
                    yield break;
                if (mAbList == null)
                    mAbList = new Dictionary<int, AB>();
                else
                    mAbList.Clear();
                yield return ParallelUtils.Schedule(() => AssetsUtil.VariantsForEditor.Initialize(), false);// ABVariant.InitVariants(false);
                var outputFolder = NativeFileSystem.GetFolderName(outputFile);
                if (Directory.Exists(outputFolder))
                    Directory.Delete(outputFolder, true);
                mInputFile = NativeFileSystem.OpenFile(abFile, FileMode.Open, FileAccess.Read);
                var reader = new StreamReader(mInputFile, Encoding.UTF8);
                string str;
                AB ab = null;
                while ((str = reader.ReadLine()) != null)
                {
                    str = str.Trim();
                    if(string.IsNullOrEmpty(str))
                    {
                        ab = null;
                        continue;
                    }
#if CONVERT_PATH
                    str = str.Replace('\\', '/');
#endif
                    string abname, variant;
                    bool getAb = false;
                    if (str.Length > 0 && str[0] == '@')
                    {
                        getAb = true;
                        AssetsUtil.VariantsForEditor.GetABInfo(str.Substring(1), out abname, out variant);
                        //ABVariant.GetABInfo(str.Substring(1), out abname, out variant);
                    }
                    else
                    {
                        AssetsUtil.VariantsForEditor.GetABInfo(str, out abname, out variant);
                        //ABVariant.GetABInfo(str, out abname, out variant);
                    }
                    if(getAb)
                    {
                        var id = ParallelUtils.IgnoreCaseToHash(abname);
                        if(!mAbList.TryGetValue(id, out ab))
                        {
                            ab = new AB(id, abname);
                            mAbList[id] = ab;
                        }
                    }
                    else if(ab != null)
                    {
                        ab.dependency.Add(abname);
                    }
                }
                mInputFile.Close();
                mInputFile.Dispose();
                mInputFile = null;
                Progress = 0.1f;
                var job = ParallelUtils.ScheduleParallel<AB>(FindDependencyCircle, mAbList.Values);

                while(job.IsAlive)
                {
                    Progress += job.GetExecution().progress * 0.5f;
                    yield return null;
                }
                if (mCircles.Count > 0)
                {
                    NativeFileSystem.EnsureDirectory(outputFolder);
                    var index = outputFile.LastIndexOf('.');
                    string ext;
                    string fileName;
                    if(index > 0)
                    {
                        ext = outputFile.Substring(index);
                        fileName = outputFile.Substring(0, index);
                    }
                    else
                    {
                        ext = ".log";
                        fileName = outputFile;
                    }
                    var num = 1;
                    var page = 1;
                    mOutputFile = NativeFileSystem.OpenFile($"{fileName}_{page}{ext}", FileMode.OpenOrCreate, FileAccess.Write);
                    var writer = new StreamWriter(mOutputFile, Encoding.UTF8);
                    writer.WriteLine($"AssetBundle Dependency Circle @{System.DateTime.Now}");
                    var progressFactor = 1f / mCircles.Count;
                    foreach(var circle in mCircles)
                    {
                        if (num % 200 == 0)
                        {
                            page++;
                            mOutputFile.Flush();
                            mOutputFile.Close();
                            mOutputFile = NativeFileSystem.OpenFile($"{fileName}_{page}{ext}", FileMode.OpenOrCreate, FileAccess.Write);
                            writer = new StreamWriter(mOutputFile, Encoding.UTF8);
                            writer.WriteLine($"AssetBundle Dependency Circle @{System.DateTime.Now}");
                        }
                        writer.WriteLine();
                        writer.WriteLine($"#{num}");
                        writer.WriteLine(circle);
                        num++;
                        Progress = 0.6f + 0.4f * num * progressFactor;
                    }
                    writer.Flush();
                    mOutputFile.Close();
                    mOutputFile.Dispose();
                    mOutputFile = null;
                    Debug.LogFormat("AB 依赖存在多处依赖闭环，详情请查看文件\"{0}\"", outputFile);
                }
                else
                {
                    Debug.Log("Perfact, AB 资源不存在依赖闭环。");
                }
            }
        }


        class ClearInvalidPrefabVariantData : ICommand, IExecutableProgress
        {
            public float Progress { get; private set; }

            void ICommand.Execute()
            {
                AssetDatabase.StartAssetEditing();
                var prefabs = AssetDatabase.FindAssets("t:prefab", new string[] { "Assets/" });
                var f = 1f / (prefabs.Length + 1f);
                int num = 0;
                foreach (var guid in prefabs)
                {
                    num++;
                    Progress = f * num;
                    var path = AssetDatabase.GUIDToAssetPath(guid);
                    var pref = AssetDatabase.LoadAssetAtPath<GameObject>(path);
                    EditorUtility.DisplayProgressBar("Clean Prefabs", path, Progress);
                    CleanPrefabInstance(pref);
                }
                AssetDatabase.StopAssetEditing();
                EditorUtility.ClearProgressBar();
            }

        }

        [MenuItem("CONTEXT/AssetImporter/Clean Prefab Instance")]
        static void CleanPrefabInstance(MenuCommand cmd)
        {
            var importor = cmd.context as AssetImporter;
            //ModelImporter
            var pref = importor == null ? null : AssetDatabase.LoadAssetAtPath<GameObject>(importor.assetPath);
            CleanPrefabInstance(pref);
        }

        static bool IsValidModify(PropertyModification modify)
        {
            var tp = modify.target == null ? null : modify.target.GetType();
            if (tp == null)
                return false;
            if (!GlobalUtil.IsTypeInheritedFrom(tp, typeof(MonoBehaviour), true))
                return true;
            var valid = false;
            var ser = new SerializedObject(modify.target);
            var iter = ser.GetIterator();
            while(!valid && iter.Next(true))
            {
                if (modify.propertyPath == iter.propertyPath)
                    valid = true;
            }
            ser.Dispose();
            return valid;
        }

        public static void CleanPrefabInstance(GameObject prefab)
        {
            if (prefab == null)
                return;
            var ptype = PrefabUtility.GetPrefabAssetType(prefab);
            if (ptype != PrefabAssetType.Variant)
            {
                return;
            }
            var modifications = PrefabUtility.GetPropertyModifications(prefab);
            var lst = new List<PropertyModification>(modifications.Length);
            lst.AddRange(modifications);
            for (int i = lst.Count - 1; i >= 0; i--)
            {
                if (!IsValidModify(lst[i]))
                    lst.RemoveAt(i);
            }
            if (lst.Count != modifications.Length)
            {
                var count = modifications.Length - lst.Count;
                PrefabUtility.SetPropertyModifications(prefab, lst.ToArray());
                bool saved;
                PrefabUtility.SavePrefabAsset(prefab, out saved);
                var path = AssetDatabase.GetAssetPath(prefab);
                if (string.IsNullOrEmpty(path))
                    path = prefab.name;
                Debug.Log($"{path}:: remove {count} invalid modification(s) and saved({saved}).", prefab);
            }
        }

        enum ProcessMatchResult
        {
            Default,
            Exclude,
            Include,
        }
        struct ABProcessPattern
        {
            public string pattern;
            public bool isRegex;
            public bool IsMatch(string path)
            {
                if (isRegex)
                    return Regex.IsMatch(path, pattern, RegexOptions.IgnoreCase);
                else
                    return pattern.EqualsIgnoreCase(path);
            }
        }


        // 预处理
        class ABPostprocess
        {
            public readonly int abid;
            public readonly string abname;

            List<ABProcessPattern> mExecludePatterns;
            List<ABProcessPattern> mIncludePatterns;

            public ABPostprocess(string abname)
            {
                this.abid = ParallelUtils.IgnoreCaseToHash(abname);
                this.abname = abname;
            }

            public void AddIncludePattern(ABProcessPattern pattern)
            {
                if (mIncludePatterns == null)
                    mIncludePatterns = new List<ABProcessPattern>();
                mIncludePatterns.Add(pattern);
            }

            public void AddExcludePattern(ABProcessPattern pattern)
            {
                if (mExecludePatterns == null)
                    mExecludePatterns = new List<ABProcessPattern>();
                mExecludePatterns.Add(pattern);
            }

            public ProcessMatchResult MatchAsset(string assetpath)
            {
                if (mExecludePatterns != null)
                {
                    for (int i = 0; i < mExecludePatterns.Count; i++)
                    {
                        if (mExecludePatterns[i].IsMatch(assetpath))
                            return ProcessMatchResult.Exclude;
                    }
                }
                if (mIncludePatterns != null)
                {
                    for (int i = 0; i < mIncludePatterns.Count; i++)
                    {
                        if (mIncludePatterns[i].IsMatch(assetpath))
                            return ProcessMatchResult.Include;
                    }
                }
                return ProcessMatchResult.Default;
            }
        }
    }
}
